Skip to content

feat(mcp): comma-split note_types/entity_types/categories in search_notes#962

Merged
groksrc merged 2 commits into
mainfrom
feat/930-comma-split-list-params
Jun 11, 2026
Merged

feat(mcp): comma-split note_types/entity_types/categories in search_notes#962
groksrc merged 2 commits into
mainfrom
feat/930-comma-split-list-params

Conversation

@groksrc

@groksrc groksrc commented Jun 11, 2026

Copy link
Copy Markdown
Member

Closes #930

Summary

Implements Option A (comma-split) for the note_types, entity_types, and categories parameters in search_notes, allowing callers to pass either a plain string ("note,canvas"), a JSON-array string ('["note","canvas"]'), or a proper list — all normalised to list[str] before validation.

Changes

  • src/basic_memory/utils.py — added parse_str_list(v): handles None → [], comma-split strings, JSON-array strings, and lists-with-comma-elements, without stripping # prefixes (unlike parse_tags). Non-str/list/None values pass through unchanged so Pydantic can still reject them.
  • src/basic_memory/mcp/tools/search.py — replaced the coerce_list BeforeValidator on note_types, entity_types, and categories with parse_str_list. Removed the now-unused coerce_list import. coerce_list itself is unchanged, so canvas and other callers are unaffected.
  • tests/test_coerce.py — added TestParseStrList covering: None, single string, comma string, spaces+comma string, JSON-array string (single/multi/comma-element), plain list, list-with-comma-element, multi-comma-element list, #-prefix preservation, and passthrough for int/dict.

Testing

All 41 tests pass; ruff lint clean.

uv run pytest tests/test_coerce.py -v   # all TestParseStrList cases green
uv run pytest                           # full suite passes

Review notes

  • Nested-list input like parse_str_list([['a','b'],'c']) produces messy stringified output ["['a'", "'b']", 'c'] because each element is str()'d before comma-splitting. This is an unrealistic input not in scope and consistent with parse_tags-style handling, so not blocking — but a future hardening could flatten nested lists.
  • Claimed test count is 18 in the implementation summary but the diff/run shows 17 TestParseStrList cases (the earlier GH comment correctly said 17). Cosmetic discrepancy only; all tests pass.

🤖 Generated with Claude Code

…otes (#930)

Add `parse_str_list` to utils.py — like `parse_tags` but without stripping
'#' — and wire it as the BeforeValidator for note_types, entity_types, and
categories in search_notes. This makes passing "note,task" or
'["note","task"]' work correctly instead of being wrapped as a single literal
value by coerce_list.

coerce_list is left unchanged; canvas and other callers that depend on its
wrap-single-string behaviour are unaffected.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Drew Cain <groksrc@gmail.com>
@groksrc groksrc marked this pull request as ready for review June 11, 2026 06:38

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 736aa840aa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# parse_str_list, not coerce_list: "note,task" must split into ["note", "task"]
# consistent with how tags are handled (#910/#930). coerce_list wraps the whole
# comma string as the single literal type ["note,task"], which matches nothing.
BeforeValidator(parse_str_list),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize comma filters outside MCP validation

This validator only runs when FastMCP/Pydantic validates a tool call, but the CLI path in src/basic_memory/cli/commands/tool.py invokes mcp_search(...) directly with repeatable options. In that path bm tool search-notes --type note,task reaches the function as note_types=["note,task"], skips this BeforeValidator, and line 904 still forwards the literal type "note,task", which matches nothing. Tags already compensate with an in-body parse_tags(tags) for the same direct-call reason; these new comma-split filters need equivalent normalization in the function body.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified and fixed in commit 0f1d728.

Claim confirmed: BeforeValidator(parse_str_list) only fires through the MCP/Pydantic path. The CLI path (cli/commands/tool.py) calls search_notes() directly, bypassing it. bm tool search-notes --type note,task arrived as note_types=["note,task"] and matched nothing, while tags already had in-body parse_tags() normalization for the same reason.

Fix: Added in-body parse_str_list() calls for note_types, entity_types, and categories in the search_notes function body, mirroring the parse_tags(tags) pattern. The validators remain for the MCP path; the body normalization covers direct callers. parse_str_list is idempotent so already-split lists pass through unchanged.

Tests added: annotation-level comma-split tests for all three params, and an async direct-call regression test (test_search_notes_direct_call_splits_comma_note_types) that calls search_notes() directly with note_types=["note,task"] and confirms it matches a real note.

In reply to: #962 (comment)

Comment thread src/basic_memory/utils.py Outdated
Comment on lines +627 to +629
for raw in v
if raw is not None
for item in str(raw).split(",")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve validation for non-string list elements

When a client sends a JSON array string or list containing non-string values, e.g. [42] or ["entity", 42], this branch stringifies each element before Pydantic sees it, so the call is accepted as filters like ["42"] instead of being rejected as invalid input. That regresses the previous coerce_list behavior and the stricter strict_search_tags boundary, causing bad caller data to become silent no-result searches; return the original value when any list element is not a string before doing comma splitting.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified and fixed in commit 0f1d728.

Claim confirmed: The list branch in parse_str_list used str(raw).split(",") which silently coerced [42] to ["42"] before Pydantic saw it, accepting invalid input as a no-result search. This regressed the strict boundary established by strict_search_tags for tags.

Fix: Added a non-string element guard at the top of the list branch in parse_str_list (utils.py). If any list element is not a string (ignoring None), the original value is returned unchanged so Pydantic rejects it with a clear validation error. Also changed str(raw).split(",") to raw.split(",") in the all-string fast path so the now-redundant str() cast is removed. All-string lists and comma-string-in-list inputs continue to work correctly.

Tests added: Three new test methods in TestParseStrList (test_coerce.py): test_int_list_passthrough_for_pydantic_rejection, test_mixed_list_passthrough_for_pydantic_rejection, test_dict_list_passthrough_for_pydantic_rejection. Three new annotation-level rejection tests in test_tool_search.py for note_types, entity_types, and categories, each confirming [42] raises ValidationError.

In reply to: #962 (comment)

… path and reject non-string list elements

Codex review of PR #962 identified two real issues:

1. CLI bypass: the BeforeValidator(parse_str_list) on note_types, entity_types, and
   categories only fires through MCP/Pydantic validation. The CLI path in
   cli/commands/tool.py calls search_notes() directly, so `bm tool search-notes
   --type note,task` arrived as note_types=["note,task"] and matched nothing.
   Fix: add in-body parse_str_list() normalization for all three params (mirroring
   the existing parse_tags() call for tags on the same code path).

2. Silent stringify: parse_str_list used str(raw) in the list branch, so [42] became
   ["42"] before Pydantic saw it, accepting invalid input as a no-result search
   instead of rejecting it. Fix: guard against non-string list elements and return
   the original value unchanged so Pydantic rejects it with a clear error.

Tests added: annotation-level split tests for note_types/entity_types/categories,
non-string-element rejection tests, async direct-call regression for note_types,
and unit-level parse_str_list non-string list tests in test_coerce.py.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Drew Cain <groksrc@gmail.com>
@groksrc groksrc merged commit 0811c48 into main Jun 11, 2026
27 checks passed
@groksrc groksrc deleted the feat/930-comma-split-list-params branch June 11, 2026 19:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Decide comma-split behavior for note_types/entity_types/categories params in search_notes

1 participant